import wsgiref.handlers
import os
import string
import urllib
from urlparse import urlparse
from sets import Set
import md5
import re
import logging
import traceback
import sys

import demjson
from BeautifulSoup import BeautifulSoup 
import feedparser

from google.appengine.ext import webapp
from google.appengine.api import users
from google.appengine.ext import db
from google.appengine.api import urlfetch
from google.appengine.ext.webapp import template
from google.appengine.ext import search

class ConciteUser(db.Expando):
	user = db.UserProperty()
	alias = db.StringProperty()
	

#class Bookmark(search.SearchableModel):
class Bookmark(db.Model):
	
	owner = db.UserProperty()
	owner_alias = db.StringProperty()
	
	uri = db.LinkProperty()
	title = db.StringProperty()
	description = db.StringProperty(multiline=True)
	
	date_published = db.DateTimeProperty(auto_now_add=True)
	date_added = db.DateTimeProperty(auto_now_add=True)
	citation = db.TextProperty()
	
	#fulltext = db.TextProperty()
	
	journalString = db.StringProperty()
	authorString = db.StringProperty()
	
	favicon = db.StringProperty()
	
	authorSort = db.StringProperty()
	
	asin = db.TextProperty()
	doi = db.TextProperty()
	pmid = db.TextProperty()

	tags = db.StringListProperty(str)	

def makeKey(owner_alias, uri):
	return "key_" + md5.new(owner_alias + "," + uri).hexdigest()
	
class connoteaAPI:
	def getBookmarks(self, user, start = 0, num = 100):
		# use public RSS feeds to import basic bookmark data for a user from Connotea rather than the API for now.
		# TODO: use the API, import the full citation, embargo details, privacy notes etc.
		bookmarks = []
		if not users.get_current_user():
			return False
		
		owner = users.get_current_user()
		owner_alias = fetchConciteUser(owner)
			
		uri = "http://www.connotea.org/rss/user/" + user + "?num=" + str(num) + "&start=" + str(start)
				
		try:
			result = urlfetch.fetch(uri)
		except Exception:
			logging.error("Couldn't get Connotea bookmark.")
			return False
			
		if result.status_code == 200:
			# parse the RSS with Mark Pilgrim's feedparser module
			rss = feedparser.parse(result.content)
			for entry in rss.entries:
				# normally we'd get the uri of the bookmark with entry.link 
				# but Connotea is *evil* in that the RSS feeds direct you back to Connotea, not the original URI
				# so we have to parse out the correct link from the <content:encoded> part of the RSS feed.
				content_encoded = entry.content[0].value	
				soup = BeautifulSoup(content_encoded)
				
				try:
					uri = soup("a", attrs={"class": "rssitem"})[0]['href']
				except:
					continue
					
				bookmark = Bookmark(key_name=makeKey(owner_alias, uri))
				bookmark.title = entry.title
				bookmark.uri = uri
				for tag in entry.tags:
					bookmark.tags.append(str(tag.term))
				bookmark.owner = owner
				bookmark.owner_alias = owner_alias
				
				bookmarks.append(bookmark)
				bookmark.put()
		else:
			return False
	
		return bookmarks
		
class removeBookmark(webapp.RequestHandler):
	def get(self):
		key = self.request.get('key')
		
		if key == "all":
			if users.is_current_user_admin():
				# delete all bookmarks
				for bookmark in Bookmark.all():
					bookmark.delete()
					
				self.redirect("/")
				return
			else:
				self.redirect("/?error=not_admin")
				return
		else:
			# delete a single key
			bookmark = Bookmark.get(key)
			if bookmark:
				bookmark.delete()
				self.redirect("/")
				return
			else:
				self.redirect("/?error=delete_fail")
				return
					
		self.redirect("/")	
		
	
def changeAlias(user, from_alias, to_alias):
	# TODO:
	# we need to get all bookmarks belonging to user and change the key_name for them to reflect the new alias.
	# we then need to update the ConciteUser entity for that user.
	return

class refreshCitation(webapp.RequestHandler):
	def get(self, key = False):
		# get citation for uri
		# update the current user's bookmark of that URI accordingly
		if not users.get_current_user():
			# you must be logged in to add a new bookmark
			self.redirect("/?error=login_required")
			return
			
		if not key:
			key = self.request.get('key')
		
		# get bookmark object
		owner = users.get_current_user()
		bookmark = Bookmark.get(key)
		
		if not bookmark:
			logging.error("Couldn't save bookmark after getting citation.")
			self.redirect("/?error=cant_cite_bookmark")
			return
		
		uri = bookmark.uri
		
		# get citation for this bookmark's uri
		payload = urllib.urlencode({'fmt': 'json', 'uri': uri})
		result = urlfetch.fetch("http://www.connotea.org/webcite", payload, urlfetch.POST)

		citation = False
		if result.status_code == 200:
			citation = demjson.decode(result.content)
			citation = citation['citation']
			
		if citation:
			bookmark.title = citation['title']
			bookmark.asin = str(citation['asin'])
			bookmark.pmid = str(citation['pubmed'])
			bookmark.doi = citation['doi']
			bookmark.journalString = citation['journal']['name']
			if citation['authors']:
				author = citation['authors'][0]
				if not author['initials']:
					author['initials'] = ""
				if not author['lastname']:
					author['lastname'] = ""
					
				authorString = str(author['initials']) + " " + str(author['lastname'])
				if len(citation['authors']) >= 2:
					authorString += " et al"
				bookmark.authorString = authorString
		
			try:
				bookmark.put()
			except Exception:
				logging.error("Couldn't save bookmark after getting citation.")
				self.redirect("/?error=put_error")
				return
				
		if self.request.get("from_bookmarklet"):
			path = os.path.join(os.path.dirname(__file__), 'bookmarklet.html')
			self.response.out.write(template.render(path, {"finished": True}))
			return
			
		self.redirect("/")
			
class addBookmark(webapp.RequestHandler):
	def post(self, uri = False, description = False, tags = False, title = False):
		if not users.get_current_user():
			# you must be logged in to add a new bookmark
			self.redirect("/?error=login_required")
			return
			
		# get the page itself, extract the title, save a bookmark 
		# skeleton then redirect to refreshCitation
		# we need to split this process up into multiple calls because of execution time
		# limits on Google App Engine.
				
		owner = users.get_current_user()
		owner_alias = fetchConciteUser(owner)
		
		if not uri:
			uri = self.request.get('uri')
			
		if not description:
			description = self.request.get('description')

		if not title:
			title = self.request.get('title')
			
		if not tags:
			tags = self.request.get('tags')
			
		key = makeKey(owner_alias, uri)

		bookmark = Bookmark(key_name=key)	
		bookmark.owner = owner
		bookmark.owner_alias = owner_alias
		
		try:
			bookmark.uri = uri
		except:
			# putting the uri in bookmark.uri will raise an exception if it's not a valid link
			self.redirect("/?error=bad_uri")
			return
					
		try:
			result = urlfetch.fetch(uri)
		except Exception:
			logging.error("Couldn't download page")
			return False

		if result.status_code == 200:
			# use BeautifulSoup to parse the HTML
			soup = BeautifulSoup(result.content)
			
			if not title:
				try:
					title = str(soup.html.head.title.string)	
				except:
					title = uri
					
			try:
				fulltext = str(soup.html.body).encode('ascii', 'ignore');
			except:
				fulltext = ""
			
			try:
				favicon = soup.find(attrs = {"rel": "shortcut icon"})['href']
			except:
				favicon = ""
			
		elif result.status_code == 302 or result.status_code == 307 or result.status_code == 303:
			# the page is a temporary redirect. Follow the redirect to the new location.
			redirect_uri = string.split(result.headers['location'], ";")
			return self.post(redirect_uri[0], self.request.get('description'), self.request.get('tags'))
		else:
			self.redirect("/?error=fetch_failed")
			return
		
		# strip carriage returns from title
		whitespace = re.compile("[\n\r]")
		title = whitespace.sub("", title)
		
		bookmark.title = title		
		#bookmark.fulltext = fulltext
		
		# did the user give us tags and a description?
		bookmark.description = description
		bookmark.favicon = favicon
		bookmark.tags = string.split(tags, "," )
	
		try:
			bookmark.put()
		except Exception:
			logging.error("Couldn't save bookmark.")
			self.redirect("/?error=put_error")
			return
			
		if self.request.get("from_bookmarklet"):
			self.redirect("/refreshCitation?from_bookmarklet=true&key=" + str(bookmark.key()))			
		else:
			self.redirect("/refreshCitation?key=" + str(bookmark.key()))
		
		return	

def fetchConciteUser(user):
	# given a Google User object find the matching record in the Concite database so that we know what to call them.
	# if they don't have an entry in the database then create one. Their default alias is their Google nickname.
	# this function should return the Concite alias of the user passed as an argument.
	alias = False
	
	query = db.GqlQuery("SELECT * FROM ConciteUser WHERE user = :1", user)
	results = query.fetch(1)
	for result in results:
		alias = result.alias
	
	if not alias:
		alias = user.nickname()
		conciteUser = ConciteUser(user = user, alias= alias)
		conciteUser.put()
		
	return alias

def createTemplate(reqHandler):
	nickname = None
	if users.get_current_user():
		url = users.create_logout_url(reqHandler.request.uri)
		nickname = fetchConciteUser(users.get_current_user())
	else:
		url = users.create_login_url(reqHandler.request.uri)
		
		
	template_values = {
		'user': nickname,
		'url': url,
		'is_admin': users.is_current_user_admin()
	}
	
	return template_values
	
class showHelp(webapp.RequestHandler):	
	def get(self):
		template_values = createTemplate(self)
		path = os.path.join(os.path.dirname(__file__), 'index_help.html')
		self.response.out.write(template.render(path, template_values))

class showBookmarklet(webapp.RequestHandler):	
	def get(self):
		template_values = createTemplate(self)
		
		template_values['uri'] = self.request.get('uri')
		template_values['title'] = self.request.get('title')		
		
		path = os.path.join(os.path.dirname(__file__), 'bookmarklet.html')
		self.response.out.write(template.render(path, template_values))
						
class showSettings(webapp.RequestHandler):	
	def post(self):
		self.get()
	def get(self):
		template_values = createTemplate(self)
		
		if not template_values['user']:
			template_values['hide_page'] = True
			template_values['error'] = "You must log on to Concite before you can access this page."
		else:
			connotea_username = self.request.get('connotea_username')
			
			if connotea_username:
				connotea = connoteaAPI()
				bookmarks = connotea.getBookmarks(connotea_username, 0, 100)
				if bookmarks:
					template_values["status"] = "Successfully imported " + str(len(bookmarks)) + " bookmarks from Connotea."				
				else:
					template_values["error"] = "Sorry, couldn't import Connotea bookmarks for that username."
					
		path = os.path.join(os.path.dirname(__file__), 'index_settings.html')
		self.response.out.write(template.render(path, template_values))
			
def prepareTagcloud(tags, user = False, bins = 4):
	# takes in a dict of tag => number of occurrences
	# returns a dict of tag => bin number
	max_tag = 0
	min_tag = 999
	
	binned_tags = {}
	
	for key in tags.keys():
		value = tags[key]
		if value > max_tag:
			max_tag = value
		if value < min_tag:
			min_tag = value
			
	# put tags into bins.
	range = max_tag - min_tag
	bin_range = range / bins
	if bin_range < 1:
		bin_range = 1
	
	for key in tags.keys():
		value = tags[key]
		bin = value / bin_range	
		path = "/tags/" + urllib.quote(key) 
		if user:
			path = path + "/" + user
			
		binned_tags[key] = "<a href='" + path + "' class='tagcloud_" + str(bin) + "'>" + key + "</a>"
	
	return binned_tags
	
class showBookmarks(webapp.RequestHandler):	
	def get(self, user = False, tag = False):
		# define some error strings to use later.
		errors = {
			"login_required": "You must log in to add new bookmarks.",
			"fetch_failed": "Couldn't find a citation for that URI, sorry.",
			"bad_uri": "The link that you supplied wasn't valid, sorry. You must provide a fully qualified URI (i.e. http://example.com/)",
			"put_error": "Sorry, the datastore couldn't save your bookmark.",
			"cant_cite_bookmark": "Sorry, we couldn't match a citation to that URI.",
			"not_admin": "Sorry, you need to be an admin to do that and you're not an admin.",
			"delete_fail": "Sorry, couldn't find a bookmark with that key."
		}
			
		# show bookmarks
		error = self.request.get('error')
		if error:
			error = errors[error]
		
		if user:
			user = urllib.unquote(user)
		
		# TODO: urlencoding of tags is a bit funny. It's quite possible that it'll break tags with slashes etc.
		# should they be allowed in the first place?
		if tag:
			tag = urllib.unquote(urllib.unquote(tag))
		
		keyword = self.request.get('q')
		
		format = self.request.get('fmt')
				
		bookmarks = Bookmark.all()
		
		if tag:
			bookmarks = bookmarks.filter('tags = ', tag)	
		if user:
			bookmarks = bookmarks.filter('owner_alias = ', user)
		
		bookmarks = bookmarks.order('-date_added')
		
		tags = {}
		for bookmark in bookmarks:
			for a_tag in bookmark.tags:
				if tags.has_key(a_tag):
					tags[a_tag] = tags[a_tag] + 1				
				else:
					tags[a_tag] = 1
		
		# turn tags into anchor tags with a tagcloud class.
		# need to do this as templates on AppEngine can't handle
		# dictionaries with arbitrary keys.
		tags = prepareTagcloud(tags)
			
		template_values = createTemplate(self)

		if user:
			template_values['filter_user'] = user
		if tag:
			template_values['filter_tag'] = tag
		if keyword:
			template_values['filter_keyword'] = keyword
			
		parsed_bookmarks = []
		for bookmark in bookmarks:
			# we can check that the bookmark isn't private etc. here.
			binned_tags = []
			for tag in bookmark.tags:
				binned_tags.append(tags[tag])
			bookmark.tags = binned_tags
			parsed_bookmarks.append(bookmark)
	
		template_values['error'] = error
		template_values['bookmarks'] = parsed_bookmarks
		template_values['request_uri'] = self.request.uri
		
		# is there already a variable on the request uri? If so append new variables with & instead of ? (I wonder if there's a way to do
		# this with Django templates?)
		parts = urlparse(self.request.uri)
		if parts.query:
			template_values['request_uri_appendchar'] = "&"
		else:
			template_values['request_uri_appendchar'] = "?"
			
		if format == "atom":
			self.response.headers["Content-Type"] = "text/xml"
			path = os.path.join(os.path.dirname(__file__), 'atom_bookmarks.xml')			
		else:
			path = os.path.join(os.path.dirname(__file__), 'index_bookmarks.html')
		
		self.response.out.write(template.render(path, template_values))

def main():
	application = webapp.WSGIApplication(
		[
			(r'/users/(.*?)/tags/(.*)', showBookmarks),
			(r'/users/(.*)(.{0})', showBookmarks),
			(r'/tags/(.{0})(.*)', showBookmarks),
			('/', showBookmarks),
			('/help', showHelp),
			('/bookmarklet', showBookmarklet),
			('/settings', showSettings),
			('/addBookmark', addBookmark),
			('/refreshCitation', refreshCitation),
			('/removeBookmark', removeBookmark)
		],
		debug=True)
	wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
	main()